package com.design_patterns.singleton;

import java.io.*;
import java.lang.reflect.Constructor;

/**
 * @author: 李德才
 * @description:
 * @create: 2020-08-23 16:37
 **/

/**
 * 单例模式的创建与攻击（反射）
 */

public class SingletonAttack {

    public static void main(String[] args) throws Exception {
        reflectionAttack();
//        serializationAttack();
    }


    /**
     * 单例模式反射攻击
     *
     * @throws Exception
     */
    public static void reflectionAttack() throws Exception {
        Constructor<DoubleCheck> constructor = DoubleCheck.class.getDeclaredConstructor();
        constructor.setAccessible(true); // 允许访问私有对象
        System.err.println(constructor.newInstance() == constructor.newInstance());
    }

    /**
     * 单例模式序列化攻击
     * @throws Exception
     */
    /**
     * 为什么会发生这种事？
     * 在ObjectInputStream.readObject()方法执行时，其内部方法readOrdinaryObject()中有这样一句话:
     * *** obj = desc.isInstantiable() ? desc.newInstance() : null; ***
     * 其中desc是类描述符。也就是说，如果一个实现了Serializable/Externalizable接口的类可以在运行时实例化，
     * 那么就调用newInstance()方法，使用其默认构造方法反射创建新的对象实例，自然也就破坏了单例性。
     * 要防御序列化攻击，就得将 instance声明为 transient，
     * private static volatile transient DoubleCheck INSTANCE;
     * transient : 将不需要序列化的属性前添加关键字transient，序列化对象的时候，这个属性就不会被序列化
     * 并且在单例中加入以下语句：
     * private Object readResolve() {
     * return instance;
     * }
     * 这是因为在上述readOrdinaryObject()方法中，
     * 会通过语句desc.hasReadResolveMethod()检查类中是否存在名为readResolve()的方法，
     * 如果有，就执行desc.invokeReadResolve(obj)调用该方法。
     * readResolve()会用自定义的反序列化逻辑覆盖默认实现，因此强制它返回instance本身，
     * 就可以防止产生新的实例
     *
     * @throws Exception
     */
    public static void serializationAttack() throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serFile"));
        DoubleCheck s1 = DoubleCheck.getInstance();
        outputStream.writeObject(s1);
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("serFile")));
        DoubleCheck s2 = (DoubleCheck) inputStream.readObject();
        System.out.println(s1 == s2);
    }
}
